In [53]:
import plotly.graph_objects as go
from IPython.display import HTML, display
# 1) Gör fig.show() självbärande (inkluderar Plotly JS inline)
def _show_inline(self, *args, **kwargs):
display(HTML(self.to_html(include_plotlyjs='inline', full_html=False)))
go.Figure.show = _show_inline
# 2) Dölj all kod från start + knapp för att visa/dölj
display(HTML("""
<script>
function toggle_code(){
const cells = document.querySelectorAll('div.input, .jp-Cell-inputWrapper');
const hide = !(cells[0] && cells[0].style.display === 'none');
for (const c of cells){ c.style.display = hide ? 'none' : ''; }
const b = document.getElementById('codebtn');
if (b) b.value = hide ? '▶ Visa kod' : '▼ Dölj kod';
}
document.addEventListener('DOMContentLoaded', () => { toggle_code(); }); // dölj från start
</script>
<form style="margin:8px 0 14px;">
<input id="codebtn" type="button" value="▶ Visa kod" onclick="toggle_code()">
</form>
"""))
In [47]:
import plotly
plotly.offline.init_notebook_mode()
In [39]:
# Imports
import math
import numpy as np
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
import math
In [40]:
# Grundparametrar
slump_seed = 42
inflation = 0.02
startkapital_msek = 7_000 # 7 BSEK
antal_ar = 15
antal_manader = antal_ar * 12
antal_simuleringar = 10_000
# Scenarier: portföljer och alpha
portfoljer = [
{"namn": "Defensiv portfölj", "mu_ar": 0.045, "sigma_ar": 0.06},
{"namn": "Hypotes", "mu_ar": 0.070, "sigma_ar": 0.12},
{"namn": "Aggressiv portfölj", "mu_ar": 0.085, "sigma_ar": 0.15},
]
alpha_lista = [0.6, 0.7, 0.8]
# Simuleringsfunktion
def simulate_monthly_paths(mu_month, sigma_month, months, n_sims, start_capital,
spending_rate, alpha, infl_annual, rng):
years = months // 12
paths = np.empty((n_sims, months+1), dtype=float)
annual_payouts = np.empty((n_sims, years), dtype=float)
for i in range(n_sims):
capital = float(start_capital)
annual_payout = spending_rate * capital # år 1 baserat på startkapital
monthly_payout = annual_payout / 12.0
paths[i, 0] = capital
for y in range(years):
annual_payouts[i, y] = annual_payout
# 12 månadssteg med fast uttag inom året
for m in range(12):
idx = y*12 + m + 1
ret = rng.normal(mu_month, sigma_month) # aritmetisk månadsreturn
capital = capital * (1 + ret) - monthly_payout
paths[i, idx] = capital
# Hybridregel (inför nästa år)
target = spending_rate * capital
annual_payout = alpha * annual_payout * (1 + infl_annual) + (1 - alpha) * target
monthly_payout = annual_payout / 12.0
return paths, annual_payouts
# Kör alla scenarier
ss = np.random.SeedSequence(slump_seed)
resultat = {}
for p in portfoljer:
mu_ar = p["mu_ar"]
sigma_ar = p["sigma_ar"]
# Spending kopplat till mu_ar: nominellt s = mu_ar - inflation (≈ realt konstant uttag)
spending_rate = mu_ar - inflation
# Månadsparametrar (aritmetisk omräkning)
mu_man = (1 + mu_ar) ** (1/12) - 1
sigma_man = sigma_ar / np.sqrt(12.0)
for a in alpha_lista:
rng_scenario = np.random.default_rng(ss.spawn(1)[0])
banor_manad, arsuttag = simulate_monthly_paths(
mu_month=mu_man, sigma_month=sigma_man,
months=antal_manader, n_sims=antal_simuleringar,
start_capital=startkapital_msek, spending_rate=spending_rate,
alpha=a, infl_annual=inflation, rng=rng_scenario
)
månader = np.arange(0, antal_manader+1)
median_m = np.median(banor_manad, axis=0)
p10_m = np.percentile(banor_manad, 10, axis=0)
p90_m = np.percentile(banor_manad, 90, axis=0)
år = np.arange(1, antal_ar+1)
arsuttag_median = np.median(arsuttag, axis=0)
arsuttag_p10 = np.percentile(arsuttag, 10, axis=0)
arsuttag_p90 = np.percentile(arsuttag, 90, axis=0)
resultat[(p["namn"], a)] = {
"paths": banor_manad,
"annual_payouts": arsuttag,
"sammanstallning": {
"månader": månader,
"median_m": median_m, "p10_m": p10_m, "p90_m": p90_m,
"år": år,
"arsuttag_median": arsuttag_median,
"arsuttag_p10": arsuttag_p10,
"arsuttag_p90": arsuttag_p90,
}
}
In [48]:
# === Gemensam x-axel (alla scenarier) ===
alla_slutkapital = np.concatenate([
res["paths"][:, -1] for res in resultat.values()
])
x_min = np.percentile(alla_slutkapital, 1)
x_max = np.percentile(alla_slutkapital, 99)
# === Definiera gemensamma bins ===
nbins = 60
bin_edges = np.linspace(x_min, x_max, nbins + 1)
bin_width = bin_edges[1] - bin_edges[0]
rows = len(portfoljer)
cols = len(alpha_lista)
# === Subplot-titlar = bara alpha ===
subplot_titles = [f"α = {a}" for _ in portfoljer for a in alpha_lista]
fig = make_subplots(
rows=rows, cols=cols,
subplot_titles=subplot_titles,
vertical_spacing=0.08, horizontal_spacing=0.05
)
for i, p in enumerate(portfoljer, start=1):
mu_pct = 100 * p["mu_ar"]
sig_pct = 100 * p["sigma_ar"]
for j, a in enumerate(alpha_lista, start=1):
res = resultat[(p["namn"], a)]
slutkapital = res["paths"][:, -1]
med = np.median(slutkapital)
p10 = np.percentile(slutkapital, 10)
p90 = np.percentile(slutkapital, 90)
# Histogram med fasta bins
fig.add_trace(
go.Histogram(
x=slutkapital,
xbins=dict(start=x_min, end=x_max, size=bin_width),
marker_color="steelblue",
opacity=0.85,
showlegend=False
),
row=i, col=j
)
# Röd linje = startkapital
fig.add_vline(
x=startkapital_msek,
line=dict(color="red", dash="dash"),
annotation_text="Start",
row=i, col=j
)
# Gemensam skala
fig.update_xaxes(range=[x_min, x_max],
title_text="Slutkapital (MSEK)" if i == rows else None,
row=i, col=j)
# Radetikett = portfölj + μ och σ
if j == 1:
fig.update_yaxes(
title_text=f"{p['namn']} – μ {mu_pct:.1f}% σ {sig_pct:.1f}%",
row=i, col=j
)
# Kort statistiktext
fig.add_annotation(
text=f"Med {med:,.0f}<br>P10–P90 {p10:,.0f}–{p90:,.0f}",
xref=f"x{(i-1)*cols+j}",
yref=f"y{(i-1)*cols+j}",
x=0.02, y=0.95,
xanchor="left", yanchor="top",
showarrow=False,
font=dict(size=10, color="black")
)
# === Layout ===
fig.update_layout(
height=950,
width=1150,
title=f"Fördelning av slutkapital efter {antal_ar} år<br>(rader = portfölj, kolumner = α; spending rate = avkastning - inflation)",
bargap=0.05,
template="plotly_white"
)
fig.show()
In [49]:
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import numpy as np
# ===============================
# Sannolikhet att kapital understiger nivåer – grid (rader=portfölj, kolumner=α)
# ===============================
nivåer = [0.9, 0.7, 0.5, 0.3]
palette = {0.9:"#636EFA", 0.7:"#00CC96", 0.5:"#FFA15A", 0.3:"#EF553B"}
rows = len(portfoljer)
cols = len(alpha_lista)
subplot_titles = [f"α = {a}" for _ in portfoljer for a in alpha_lista]
fig = make_subplots(
rows=rows, cols=cols,
subplot_titles=subplot_titles,
vertical_spacing=0.08, horizontal_spacing=0.05
)
# --- välj "rimlig" tick-steg baserat på radens max ---
def pick_dtick(ymax):
if ymax <= 25: return 5
if ymax <= 50: return 10
return 20
# --- beräkna max-sannolikhet per rad (portfölj) ---
row_ymax = []
for p in portfoljer:
y_max = 0.0
for a in alpha_lista:
banor = resultat[(p["namn"], a)]["paths"]
for t in nivåer:
y_max = max(y_max, (banor < startkapital_msek*t).mean(axis=0).max()*100)
y_max = min(100.0, np.ceil(y_max/5)*5) # runda upp till närmaste 5-tal, cap 100 %
row_ymax.append(y_max)
# --- rita paneler ---
for i, p in enumerate(portfoljer, start=1):
mu_pct = 100 * p["mu_ar"]
sig_pct = 100 * p["sigma_ar"]
y_cap = row_ymax[i-1]
dtick = pick_dtick(y_cap)
for j, a in enumerate(alpha_lista, start=1):
banor = resultat[(p["namn"], a)]["paths"]
månader = np.arange(banor.shape[1])
for t in nivåer:
sannolikhet = (banor < startkapital_msek*t).mean(axis=0) * 100
fig.add_trace(
go.Scatter(
x=månader, y=sannolikhet,
mode="lines",
line=dict(color=palette[t], width=2),
name=f"< {int(t*100)} %",
showlegend=(i == 1 and j == 1)
),
row=i, col=j
)
fig.update_xaxes(
title_text="Månader" if i == rows else None,
range=[0, månader[-1]],
row=i, col=j
)
fig.update_yaxes(
title_text=(f"{p['namn']} – μ {mu_pct:.1f}% σ {sig_pct:.1f}%" if j == 1 else None),
range=[0, y_cap],
dtick=dtick,
row=i, col=j
)
# --- layout ---
fig.update_layout(
height=950,
width=1200,
template="plotly_white",
title="Sannolikhet att kapital understiger olika nivåer<br>(rader = portfölj, kolumner = α)",
legend_title="Kapitalnivå",
legend=dict(orientation="h", x=0.5, y=-0.08, xanchor="center", yanchor="top"),
hovermode="x unified",
font=dict(family="Inter, Segoe UI, Roboto, Arial", size=12),
margin=dict(l=120, r=40, t=90, b=70)
)
fig.show()
In [50]:
# ===============================
# Fan chart: Årligt uttag – grid (rader=portfölj, kolumner=α)
# ===============================
rows = len(portfoljer)
cols = len(alpha_lista)
subplot_titles = [f"α = {a}" for _ in portfoljer for a in alpha_lista]
fig = make_subplots(
rows=rows, cols=cols,
subplot_titles=subplot_titles,
vertical_spacing=0.08, horizontal_spacing=0.05
)
# Färger
median_color = "#636EFA" # blå
outer_fill = "rgba(99,110,250,0.20)" # P10–P90
inner_fill = "rgba(99,110,250,0.35)" # P25–P75
years_idx = np.arange(1, antal_ar + 1)
def nice_step(ymax, max_ticks=6):
"""Välj ett 'snyggt' tick-steg så att antalet ticks ≲ max_ticks."""
if ymax <= 0:
return 1.0
# Kandidatsteg i MSEK
candidates = [0.5, 1, 2, 5, 10, 20, 25, 50, 100, 200, 250, 500, 1000, 2000, 5000]
for s in candidates:
if ymax / s <= max_ticks:
return s
# Nödfall: grovt steg
pow10 = 10 ** math.ceil(math.log10(ymax / max_ticks))
return pow10
# --- Radvis y-topp och tick-steg ---
row_settings = []
for p in portfoljer:
y_max = 0.0
for a in alpha_lista:
payouts = resultat[(p["namn"], a)]["annual_payouts"] # (n_sims, years)
y_max = max(y_max, np.percentile(payouts, 90, axis=0).max())
y_max *= 1.05 # lite luft
step = nice_step(y_max, max_ticks=6)
y_top = math.ceil(y_max / step) * step
row_settings.append((y_top, step))
# --- Rita paneler ---
for i, p in enumerate(portfoljer, start=1):
mu_pct = 100 * p["mu_ar"]
sig_pct = 100 * p["sigma_ar"]
y_top, y_step = row_settings[i-1]
for j, a in enumerate(alpha_lista, start=1):
payouts = resultat[(p["namn"], a)]["annual_payouts"]
q10 = np.percentile(payouts, 10, axis=0)
q25 = np.percentile(payouts, 25, axis=0)
q50 = np.percentile(payouts, 50, axis=0)
q75 = np.percentile(payouts, 75, axis=0)
q90 = np.percentile(payouts, 90, axis=0)
# Yttre band P10–P90
fig.add_trace(go.Scatter(x=years_idx, y=q90, mode="lines",
line=dict(width=0), showlegend=False),
row=i, col=j)
fig.add_trace(go.Scatter(x=years_idx, y=q10, mode="lines",
fill="tonexty", fillcolor=outer_fill,
line=dict(width=0),
name="P10–P90", showlegend=(i == 1 and j == 1)),
row=i, col=j)
# Inre band P25–P75
fig.add_trace(go.Scatter(x=years_idx, y=q75, mode="lines",
line=dict(width=0), showlegend=False),
row=i, col=j)
fig.add_trace(go.Scatter(x=years_idx, y=q25, mode="lines",
fill="tonexty", fillcolor=inner_fill,
line=dict(width=0),
name="P25–P75", showlegend=(i == 1 and j == 1)),
row=i, col=j)
# Median
fig.add_trace(go.Scatter(x=years_idx, y=q50, mode="lines",
line=dict(color=median_color, width=2.2),
name="Median", showlegend=(i == 1 and j == 1)),
row=i, col=j)
# Axlar
fig.update_xaxes(title_text="År" if i == rows else None,
tickmode="linear", tick0=1, dtick=1,
range=[1, antal_ar],
row=i, col=j)
fig.update_yaxes(title_text=(f"{p['namn']} – μ {mu_pct:.1f}% σ {sig_pct:.1f}%" if j == 1 else None),
range=[0, y_top], dtick=y_step,
tickformat=",.0f", # heltal MSEK
row=i, col=j)
# Gemensam Y-etikett
fig.add_annotation(text="Årligt uttag (MSEK)",
xref="paper", yref="paper",
x=-0.07, y=0.5, textangle=-90,
showarrow=False, font=dict(size=13))
# Layout
fig.update_layout(
height=950,
width=1200,
template="plotly_white",
title="Fan chart – årligt uttag (rader = portfölj, kolumner = α)",
legend=dict(orientation="h", x=0.5, y=-0.08, xanchor="center", yanchor="top"),
hovermode="x unified",
font=dict(family="Inter, Segoe UI, Roboto, Arial", size=12),
margin=dict(l=120, r=40, t=90, b=70)
)
fig.show()
In [51]:
# ============
# 100 uttagsbanor (årligt) – grid
# ============
rows = len(portfoljer)
cols = len(alpha_lista)
subplot_titles = [f"α = {a}" for _ in portfoljer for a in alpha_lista]
fig = make_subplots(
rows=rows, cols=cols,
subplot_titles=subplot_titles,
vertical_spacing=0.08, horizontal_spacing=0.05
)
rng = np.random.default_rng(slump_seed)
years_idx = np.arange(1, antal_ar + 1)
# Hjälp: "snyggt" tick-steg
def nice_step(ymax, max_ticks=6):
if ymax <= 0: return 1.0
candidates = [0.5,1,2,5,10,20,25,50,100,200,250,500,1000,2000,5000]
for s in candidates:
if ymax / s <= max_ticks: return s
pow10 = 10 ** math.ceil(math.log10(ymax / max_ticks))
return pow10
# Radvis y-top (95:e percentilen över alla α) för rimlig skala
row_settings = []
for p in portfoljer:
all_vals = []
for a in alpha_lista:
payouts = resultat[(p["namn"], a)]["annual_payouts"] # (n_sims, years)
all_vals.append(payouts.ravel())
all_vals = np.concatenate(all_vals)
y_max = np.percentile(all_vals, 95) * 1.05
step = nice_step(y_max, max_ticks=6)
y_top = math.ceil(y_max / step) * step
row_settings.append((y_top, step))
# Rita paneler
for i, p in enumerate(portfoljer, start=1):
mu_pct = 100 * p["mu_ar"]
sig_pct = 100 * p["sigma_ar"]
y_top, y_step = row_settings[i-1]
for j, a in enumerate(alpha_lista, start=1):
payouts = resultat[(p["namn"], a)]["annual_payouts"] # (n_sims, years)
n_sims = payouts.shape[0]
n_plot = min(100, n_sims)
idx = rng.choice(n_sims, size=n_plot, replace=False)
# Slumpbanor (tunna grå linjer)
for k in idx:
fig.add_trace(
go.Scatter(
x=years_idx, y=payouts[k, :],
mode="lines",
line=dict(color="rgba(140,140,140,0.35)", width=1),
showlegend=False
),
row=i, col=j
)
# Median (accent)
q50 = np.percentile(payouts, 50, axis=0)
fig.add_trace(
go.Scatter(
x=years_idx, y=q50,
mode="lines",
line=dict(color="#636EFA", width=2.2),
name="Median", showlegend=(i == 1 and j == 1)
),
row=i, col=j
)
# Axlar
fig.update_xaxes(
title_text="År" if i == rows else None,
tickmode="linear", tick0=1, dtick=1,
range=[1, antal_ar],
row=i, col=j
)
fig.update_yaxes(
title_text=(f"{p['namn']} – μ {mu_pct:.1f}% σ {sig_pct:.1f}%" if j == 1 else None),
range=[0, y_top], dtick=y_step,
tickformat=",.0f",
row=i, col=j
)
# Gemensam Y-etikett
fig.add_annotation(
text="Årligt uttag (MSEK)",
xref="paper", yref="paper",
x=-0.07, y=0.5, textangle=-90,
showarrow=False, font=dict(size=13)
)
# Layout
fig.update_layout(
height=950,
width=1200,
template="plotly_white",
title="100 slumpmässiga uttagsbanor – årligt uttag (rader = portfölj, kolumner = α)",
legend=dict(orientation="h", x=0.5, y=-0.08, xanchor="center", yanchor="top"),
hovermode="x unified",
font=dict(family="Inter, Segoe UI, Roboto, Arial", size=12),
margin=dict(l=120, r=40, t=90, b=70)
)
fig.show()
In [ ]:
In [ ]: